package cn.szv5;

import java.util.LinkedList;

/**
 * @ClassName FSEditlog
 * @Description 负责管理edit log日志的核心组件
 * @Author Carl
 * @Company: 阿飞工作室(https : / / szv5.cn)
 * @Date 2022/3/1 20:07
 * @Version 1.0
 **/
public class FSEditlog {

    /**
     * 当前递增到的txid的序号
     */
    private long txidSeq = 0L;

    /**
     * 当前是不在将内存缓冲刷入磁盘
     */
    private volatile boolean isSyncRunning = false;

    /**
     * 当前是否有线程在等待刷新下一批edits log到磁盘里去
     */
    private volatile boolean isWaitSync = false;

    /**
     * 在同步到磁盘中的最大的一个txid
     */
    private volatile long syncMaxTxid = 0L;

    /**
     * 每个线程自己本地的txid副本
     */
    private ThreadLocal<Long> localTxid = new ThreadLocal<Long>();

    /**
     * 内存双缓冲区
     */
    private DoubleBuffer editLogBuffer = new DoubleBuffer();

    /**
     * 记录edit log
     *
     * @param cotent
     */
    public void logEdit(String cotent) {
        synchronized (this) {
            // 获取全局唯一递增的txid，代表了edits log的序号
            txidSeq++;
            long txid = txidSeq;
            localTxid.set(txid);

            EditLog log = new EditLog(txid, cotent);
            // 将edits log写入内存缓冲中，不是直接刷入磁盘文件
            editLogBuffer.write(log);
        }
        logSync();

        synchronized (this) {
            // 同步完了磁盘之后，就会将标志位复位，再释放锁
            isSyncRunning = false;
            // 唤醒可能正在等待他同步完磁盘的线程
            notifyAll();
        }
    }

    /**
     * 批量将内存缓冲区数据刷入磁盘
     */
    private void logSync() {
        // 再次尝试加锁
        synchronized (this) {
            if (isSyncRunning) {
                // 假如说某个线程已经把txid = 1,2,3,4,5的edits log都从syncBuffer刷入磁盘了
                // 此时syncMaxTxid = 5，代表的是正在输入磁盘的最大txid
                // 那么这个时候来一个线程，他对应的txid = 3，此时他是可以直接返回了
                // 就代表说肯定是他对应的edits log已经被别的线程在刷入磁盘了
                // 这个时候txid = 3的线程就不需要等待了
                long txid = localTxid.get();
                if (txid < syncMaxTxid) {
                    return;
                }

                // 假如说此时来一个txid = 6的线程，那么的话，他是不好说的
                // 他就需要做一些等待，同时要释放掉锁
                if (isWaitSync) {
                    return;
                }
                isWaitSync = true;
                while (isSyncRunning) {
                    try {
                        wait(2000);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
                isWaitSync = false;

            }

            // 交换两块缓冲区
            editLogBuffer.setReadyToSync();
            syncMaxTxid = editLogBuffer.getSyncMaxTxid();
            // 设置当前正在同步到磁盘的标志位
            isSyncRunning = true;
        }

        //写磁盘
        editLogBuffer.flush();
    }

    /**
     * 代表一条edit log
     */
    class EditLog {
        long txid;
        String content;

        public EditLog(long txid, String content) {
            this.txid = txid;
            this.content = content;
        }

        @Override
        public String toString() {
            return "EditLog{" + "txid=" + txid + ", content='" + content + '\'' + '}';
        }

    }

    /**
     * 内存双缓冲
     */
    class DoubleBuffer {

        /**
         * 写缓冲区
         */
        LinkedList<EditLog> currentBuffer = new LinkedList<EditLog>();

        /**
         * 数据同步到磁盘缓冲区
         */
        LinkedList<EditLog> syncBuffer = new LinkedList<EditLog>();

        /**
         * 写缓冲
         *
         * @param log
         */
        public void write(EditLog log) {
            currentBuffer.add(log);
        }

        /**
         * 交换两块缓冲区数据，为同步数据到磁盘做准备
         */
        public void setReadyToSync() {
            LinkedList<EditLog> tmp = currentBuffer;
            currentBuffer = syncBuffer;
            syncBuffer = tmp;
        }

        /**
         * 获取sync buffer缓冲区里最大的一个txid
         *
         * @return
         */
        public long getSyncMaxTxid() {
            return syncBuffer.getLast().txid;
        }

        /**
         * 将syncBuffer缓冲区内容刷入硬盘
         */
        public void flush() {
            for (EditLog log : syncBuffer) {
                System.out.println("将edit log写入磁盘：" + log);
                // TODO 文件流输出
            }
            syncBuffer.clear();
        }

    }

}
